Skip to content

feat: numberfield announce pasted value#8604

Merged
LFDanLu merged 17 commits intomainfrom
numberfield-paste-announcements
Feb 6, 2026
Merged

feat: numberfield announce pasted value#8604
LFDanLu merged 17 commits intomainfrom
numberfield-paste-announcements

Conversation

@snowystinger
Copy link
Member

Closes #4967

Split out of #6520

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@rspbot
Copy link

rspbot commented Jul 24, 2025

Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how best should we go about testing that this works with all kinds of locales/number formats? I was able to confirm the behavior with values like 3.000.000,25 and 1.000, but I guess it might be good enough that those pass since the parsing/formatting/validating is tested elsewhere

Comment on lines 200 to 202
let value = state.parser.parse(pastedText);
let reformattedValue = numberFormatter.format(value);
if (state.validate(reformattedValue)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does there exist a case where the pastedText is considered valid but is unable to be parsed? The description of parse seems to suggest that might be the case, but digging to see if that is the case. Just concerned if there are any cases where we make it here but end up not announcing that we couldn't parse the value

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be

"numberField": "حقل رقمي"
"numberField": "حقل رقمي",
"pastedValue": "Pasted value: {value}",
"couldNotParseValue": "Could not understand value: {value}, try another format perhaps"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mega nit for more assertive message haha:

Suggested change
"couldNotParseValue": "Could not understand value: {value}, try another format perhaps"
"couldNotParseValue": "Could not understand value: {value}, please try another number format"

@devongovett
Copy link
Member

Couldn't the original problem from #4967 still exist? If you typed the value (rather than pasted) and then blurred, and our parsing resulted in something different, it wouldn't get announced. Should we just announce the formatted value on blur instead of paste?

@snowystinger
Copy link
Member Author

snowystinger commented Jul 26, 2025

Couldn't the original problem from #4967 still exist? If you typed the value (rather than pasted) and then blurred, and our parsing resulted in something different, it wouldn't get announced. Should we just announce the formatted value on blur instead of paste?

Yeah, I was debating this, as the changes also exposed parse on the state object, which I wasn't thrilled with, in case the parser suddenly changed one render to another

announcing in onBlur isn't quite right either, we don't know if the value is going to be accepted by a controlled component, so in that case the during paste was more "correct"

we could do it in an effect after, but then it gets hard to know if we're just announcing typing (based on inputValue) or if we base it on value, then we may get no announcement if it failed to write a new number and reverted

# Conflicts:
#	packages/@react-aria/numberfield/package.json
#	yarn.lock
@rspbot
Copy link

rspbot commented Jul 26, 2025

@devongovett
Copy link
Member

announcing in onBlur isn't quite right either, we don't know if the value is going to be accepted by a controlled component

Maybe something like this?

let onBlur = () => {
  let oldValue = inputRef.current.value;
  flushSync(() => commit());
  if (inputRef.current.value !== oldValue) {
    announce(inputRef.current.value);
  }
};

Though I guess it would be better to detect if the actual number value changed rather than just the text, as it could have just re-formatted but still represent the same value.

@snowystinger
Copy link
Member Author

@devongovett I've updated the code, see if you like this better in terms of when announcements happen
I've moved the paste logic around a little so that you get immediate commit and feedback without needing to parse out of turn

@rspbot
Copy link

rspbot commented Aug 22, 2025

Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the behavior using 1,000 in de-DE and a couple other combinations of locales and number formats. I'm a bit unsure about the blur announce behavior since a VO user on keyboard would typically tab to a new element (I assume) and thus never hear the announcement as you noted. I was only able to hear it by clicking off the input onto the empty background.

The paste operation immediately formatting the value is fine I think though

# Conflicts:
#	packages/@react-aria/numberfield/package.json
#	yarn.lock
@rspbot
Copy link

rspbot commented Aug 27, 2025

@rspbot
Copy link

rspbot commented Sep 11, 2025

@LFDanLu
Copy link
Member

LFDanLu commented Sep 17, 2025

Verified the behavior using 1,000 in de-DE and a couple other combinations of locales and number formats. I'm a bit unsure about the blur announce behavior since a VO user on keyboard would typically tab to a new element (I assume) and thus never hear the announcement as you noted. I was only able to hear it by clicking off the input onto the empty background.

The paste operation immediately formatting the value is fine I think though

Any thoughts about the above? Dunno if making the announcement for VO is worth it if they won't hear it when focusing a new element, unless there is some other interaction I'm not thinking of here?

@snowystinger
Copy link
Member Author

Verified the behavior using 1,000 in de-DE and a couple other combinations of locales and number formats. I'm a bit unsure about the blur announce behavior since a VO user on keyboard would typically tab to a new element (I assume) and thus never hear the announcement as you noted. I was only able to hear it by clicking off the input onto the empty background.
The paste operation immediately formatting the value is fine I think though

Any thoughts about the above? Dunno if making the announcement for VO is worth it if they won't hear it when focusing a new element, unless there is some other interaction I'm not thinking of here?

They hear it when clicking off, so I think that's helpful at the very least. Yeah, otherwise not sure what we can do there. Possibly the work from #8776 will help with min/max at least, but the rest, i'm not sure.

LFDanLu
LFDanLu previously approved these changes Oct 4, 2025
Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still on the fence about the VO behavior but I guess it is an improvement that it at least announces on blur via click

# Conflicts:
#	packages/@react-aria/numberfield/package.json
#	yarn.lock
@rspbot
Copy link

rspbot commented Jan 6, 2026

# Conflicts:
#	packages/react-aria-components/test/NumberField.test.js
LFDanLu
LFDanLu previously approved these changes Jan 29, 2026
Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sanity checked that behavior was still fine after the merge

@rspbot
Copy link

rspbot commented Jan 29, 2026

});
// Note: this announcement will be skipped if the user is keyboard navigating to a new
// focusable element because that target will be announced instead, even though this is
// assertive. This is expected VO behaviour.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the common case though, so we need to delay it or something?

Copy link
Member Author

@snowystinger snowystinger Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some testing on the newest iOS+VO and MacOS+VO and this statement doesn't appear to be true anymore.
Also checked with Android + TalkBack on the latest OS there

@rspbot
Copy link

rspbot commented Feb 1, 2026

# Conflicts:
#	packages/@react-aria/numberfield/package.json
#	yarn.lock
@rspbot
Copy link

rspbot commented Feb 4, 2026

@rspbot
Copy link

rspbot commented Feb 4, 2026

## API Changes

react-aria-components

/react-aria-components:NumberFieldState

 NumberFieldState {
   canDecrement: boolean
   canIncrement: boolean
-  commit: () => void
+  commit: (string) => void
   commitValidation: () => void
   decrement: () => void
   decrementToMin: () => void
   defaultNumberValue: number
   increment: () => void
   incrementToMax: () => void
   inputValue: string
   maxValue?: number
   minValue?: number
   numberValue: number
   realtimeValidation: ValidationResult
   resetValidation: () => void
   setInputValue: (string) => void
   setNumberValue: (number) => void
   updateValidation: (ValidationResult) => void
   validate: (string) => boolean
 }

@react-stately/color

/@react-stately/color:ColorChannelFieldState

 ColorChannelFieldState {
   canDecrement: boolean
   canIncrement: boolean
   colorValue: Color
-  commit: () => void
+  commit: (string) => void
   commitValidation: () => void
   decrement: () => void
   decrementToMin: () => void
   defaultColorValue: Color | null
   displayValidation: ValidationResult
   increment: () => void
   incrementToMax: () => void
   inputValue: string
   maxValue?: number
   minValue?: number
   numberValue: number
   realtimeValidation: ValidationResult
   resetValidation: () => void
   setColorValue: (Color | null) => void
   setInputValue: (string) => void
   setNumberValue: (number) => void
   updateValidation: (ValidationResult) => void
   validate: (string) => boolean
 }

@react-stately/numberfield

/@react-stately/numberfield:NumberFieldState

 NumberFieldState {
   canDecrement: boolean
   canIncrement: boolean
-  commit: () => void
+  commit: (string) => void
   commitValidation: () => void
   decrement: () => void
   decrementToMin: () => void
   defaultNumberValue: number
   increment: () => void
   incrementToMax: () => void
   inputValue: string
   maxValue?: number
   minValue?: number
   numberValue: number
   realtimeValidation: ValidationResult
   resetValidation: () => void
   setInputValue: (string) => void
   setNumberValue: (number) => void
   updateValidation: (ValidationResult) => void
   validate: (string) => boolean
 }

Copy link
Member

@devongovett devongovett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to only announce sometimes when I tab away in Safari, not exactly sure why. But it's better than before.

@LFDanLu LFDanLu added this pull request to the merge queue Feb 6, 2026
Merged via the queue into main with commit e013394 Feb 6, 2026
29 checks passed
@LFDanLu LFDanLu deleted the numberfield-paste-announcements branch February 6, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useNumberField mutates number so screenreader users won’t know that their value has changed

5 participants